package com.har01d.lang.compiler.domain;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.har01d.lang.compiler.domain.function.Argument;
import com.har01d.lang.compiler.domain.function.Function;
import com.har01d.lang.compiler.domain.function.FunctionParameter;
import com.har01d.lang.compiler.domain.function.FunctionReference;
import com.har01d.lang.compiler.domain.function.FunctionSignature;
import com.har01d.lang.compiler.domain.type.ClassType;
import com.har01d.lang.compiler.domain.type.FunctionType;
import com.har01d.lang.compiler.domain.type.Type;
import com.har01d.lang.compiler.domain.variable.Field;
import com.har01d.lang.compiler.domain.variable.LocalVariable;

public class Scope {

    private final Scope parent;
    private final ScopeType type;
    private final MetaData metaData;
    private final Map<String, Field> fields;
    private final Map<String, LocalVariable> localVariables;
    private final List<String> localVariablesIndex;
    private final Set<FunctionSignature> functionSignatures;
    private final Set<LocalVariable> implicitVariables = new HashSet<>();
    private final List<ClassDeclaration> classDeclarations;

    private FunctionSignature functionSignature;

    public Scope(MetaData metaData, ScopeType type, Scope parent) {
        this.metaData = metaData;
        this.parent = parent;
        this.type = type;
        localVariables = new HashMap<>();
        fields = new HashMap<>();
        localVariablesIndex = new ArrayList<>();
        classDeclarations = parent != null ? parent.classDeclarations : new ArrayList<>();
        functionSignatures = new HashSet<>();
    }

    private Scope(Scope parent, ScopeType type, boolean isFunction) {
        this.metaData = parent.metaData;
        this.parent = parent;
        this.type = type;
        if (isFunction) {
            localVariables = new HashMap<>();
            localVariablesIndex = new ArrayList<>();
            functionSignatures = new HashSet<>();
        } else {
            localVariables = new HashMap<>(parent.localVariables);
            localVariablesIndex = new ArrayList<>(parent.localVariablesIndex);
            functionSignatures = new HashSet<>(parent.functionSignatures);
        }
        fields = new HashMap<>(parent.fields);
        classDeclarations = parent.classDeclarations;
        functionSignature = parent.functionSignature;
    }

    public Scope block() {
        return new Scope(this, ScopeType.BLOCK, false);
    }

    public Scope loop() {
        return new Scope(this, ScopeType.LOOP, false);
    }

    public Scope function() {
        return new Scope(this, ScopeType.FUNCTION, true);
    }

    public Scope constructor() {
        return new Scope(this, ScopeType.CONSTRUCTOR, true);
    }

    public ScopeType getType() {
        return type;
    }

    public String getFunctionName() {
        return functionSignature == null ? null : functionSignature.getInternalName();
    }

    public FunctionSignature getFunctionSignature() {
        return functionSignature;
    }

    public FunctionSignature getSignature(FunctionSignature functionSignature) {
        for (FunctionSignature signature : functionSignatures) {
            if (signature.equals(functionSignature)) {
                return signature;
            }
        }
        return functionSignature;
    }

    public void setFunctionSignature(FunctionSignature functionSignature) {
        this.functionSignature = functionSignature;
    }

    public void addSignature(FunctionSignature functionSignature) {
        functionSignatures.add(functionSignature);
    }

    public boolean isSignatureExist(FunctionSignature signature) {
        return functionSignatures.contains(signature);
    }

    public FunctionSignature getSignature(String name, List<Argument> arguments) {
        for (FunctionSignature signature : functionSignatures) {
            if (signature.matches(name, arguments)) {
                return signature;
            }
        }

        if (parent != null) {
            return parent.getSignature(name, arguments);
        }

        return null;
    }

    public FunctionSignature getSignature(Type type, String name, List<Argument> arguments) {
        if (metaData.getClassName().equals(type.getName())) {
            return getSignature(name, arguments);
        }

        for (ClassDeclaration classDeclaration : classDeclarations) {
            if (classDeclaration.getType().equals(type)) {
                for (Function function : classDeclaration.getMethods()) {
                    if (function.getFunctionSignature().matches(name, arguments)) {
                        return function.getFunctionSignature();
                    }
                }
            }
        }
        return null;
    }

    public FunctionReference getFunctionReference(String name) {
        for (FunctionSignature signature : functionSignatures) {
            if (signature.getName().equals(name)) {
                return new FunctionReference(signature);
            }
        }

        if (functionSignature != null) {
            for (FunctionParameter parameter : functionSignature.getParameters()) {
                if (parameter.getName().equals(name) && parameter.getType() instanceof FunctionType) {
                    return new FunctionReference((FunctionType) parameter.getType());
                }
            }
        }
        return null;
    }

    public boolean isLocalVariableExists(String name) {
        return localVariables.containsKey(name);
    }

    public LocalVariable getLocalVariable(String name) {
        return localVariables.get(name);
    }

    public LocalVariable getVariable(String name) {
        if (localVariables.containsKey(name)) {
            return localVariables.get(name);
        }

        if (parent != null) {
            return parent.getVariable(name);
        }
        return null;
    }

    public LocalVariable addLocalVariable(String name, Type type, boolean initialized) {
        LocalVariable variable = new LocalVariable(name, type, initialized, false, this);
        variable.setIndex(localVariablesIndex.size());
        localVariables.put(name, variable);
        localVariablesIndex.add(name);
        return variable;
    }

    public LocalVariable addLocalValue(String name, Type type, boolean initialized) {
        LocalVariable variable = new LocalVariable(name, type, initialized, true, this);
        variable.setIndex(localVariablesIndex.size());
        localVariables.put(name, variable);
        localVariablesIndex.add(name);
        return variable;
    }

    public void addImplicitVariable(LocalVariable variable) {
        implicitVariables.add(variable);
        addLocalValue(variable.getName(), variable.getType(), true);
    }

    public Set<LocalVariable> getImplicitVariables() {
        return implicitVariables;
    }

    public Type getClassType() {
        return new ClassType(metaData.getClassName());
    }

    public String getClassName() {
        return metaData.getClassName();
    }

    public boolean isClassDeclaration() {
        return metaData.isClassDeclaration();
    }

    public void addClassDeclaration(ClassDeclaration classDeclaration) {
        classDeclarations.add(classDeclaration);
    }

    public FunctionSignature getConstructor(String name, List<Argument> arguments) {
        if (classDeclarations == null) {
            return null;
        }

        for (ClassDeclaration declaration : classDeclarations) {
            if (declaration.getName().equals(name)) {
                for (Function function : declaration.getConstructors()) {
                    if (function.getFunctionSignature().matches(name, arguments)) {
                        return function.getFunctionSignature();
                    }
                }
            }
        }

        return null;
    }

    public boolean isFieldExist(String name) {
        return fields.containsKey(name);
    }

    public Field getField(String name) {
        return fields.get(name);
    }

    public Field addField(String name, Type type, boolean initialized, boolean readonly) {
        Field field = new Field(name, type, initialized, readonly, this);
        fields.put(name, field);
        return field;
    }

}
